/*
 * Copyright 2011 JessTsai(simpleisgood1121@gmail.com)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.user.client.ui;

import java.util.Date;

import com.cleangwt.uibinder.client.TemplateSupport;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.i18n.client.TimeZone;
import com.google.gwt.text.shared.AbstractRenderer;
import com.google.gwt.text.shared.Parser;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ValueBoxBase;
import com.google.gwt.user.datepicker.client.DatePicker;

/**
 * A new implementation of {@link com.google.gwt.user.datepicker.client.DateBox}, supports element
 * wrapping.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
public class DateBox extends ValueBoxBase<Date> {
  private class DateBoxHandler implements ValueChangeHandler<Date>, FocusHandler, BlurHandler,
      ClickHandler, KeyDownHandler, CloseHandler<PopupPanel> {

    @Override
    public void onBlur(BlurEvent event) {
      if (isDatePickerShowing() == false) {
        updateDateFromTextBox();
      }
    }

    @Override
    public void onClick(ClickEvent event) {
      showDatePicker();
    }

    @Override
    public void onClose(CloseEvent<PopupPanel> event) {
      // If we are not closing because we have picked a new value, make
      // sure the
      // current value is updated.
      if (allowDPShow) {
        updateDateFromTextBox();
      }
    }

    @Override
    public void onFocus(FocusEvent event) {
      if (allowDPShow && isDatePickerShowing() == false) {
        showDatePicker();
      }
    }

    @Override
    public void onKeyDown(KeyDownEvent event) {
      switch (event.getNativeKeyCode()) {
        case KeyCodes.KEY_ENTER:
        case KeyCodes.KEY_TAB:
          updateDateFromTextBox();
          // Deliberate fall through
        case KeyCodes.KEY_ESCAPE:
        case KeyCodes.KEY_UP:
          hideDatePicker();
          break;
        case KeyCodes.KEY_DOWN:
          showDatePicker();
          break;
      }
    }

    @Override
    public void onValueChange(ValueChangeEvent<Date> event) {
      setValue(event.getValue());
      hideDatePicker();
      preventDatePickerPopup();
      setFocus(true);
    }
  }

  private static class DateParser implements Parser<Date> {
    private DateTimeFormat dateTimeFormat;

    private DateParser(DateTimeFormat dateTimeFormat) {
      this.dateTimeFormat = dateTimeFormat;
    }

    @SuppressWarnings("deprecation")
    @Override
    public Date parse(CharSequence object) {
      String text = object.toString().trim();
      if ("".equals(text)) {
        return null;
      }
      Date date = null;
      try {
        if (text.length() > 0) {
          date = dateTimeFormat.parse(text);
        }
      } catch (IllegalArgumentException exception) {
        try {
          date = new Date(text);
        } catch (IllegalArgumentException e) {
        }
      }
      return date;
    }

  }

  /**
   * Renders {@link Date} objects with a {@link DateTimeFormat}.
   */
  private static class DateTimeRenderer extends AbstractRenderer<Date> {
    private final DateTimeFormat format;
    private final TimeZone timeZone;

    /**
     * Create an instance with the given format and the default time zone.
     */
    public DateTimeRenderer(DateTimeFormat format) {
      this(format, null);
    }

    /**
     * Create an instance with the given format and time zone.
     */
    public DateTimeRenderer(DateTimeFormat format, TimeZone timeZone) {
      assert format != null;
      this.format = format;
      this.timeZone = timeZone;
    }

    @Override
    public String render(Date object) {
      if (object == null) {
        return "";
      }
      return format.format(object, timeZone);
    }
  }

  /**
   * Creates a DateBox widget that wraps an existing &lt;input&gt; element of template.
   * 
   * @param template the template
   * @param element the element to be wrapped
   */
  public static DateBox wrap(TemplateSupport template, Element element) {
    DateBox widget = new DateBox(element);
    template.wrap(widget);
    return widget;
  }

  private static final String DEFAULT_STYLENAME = "gwt-DateBox";
  private boolean allowDPShow = true;
  private final DateTimeFormat dateTimeFormat;
  private final DatePicker picker;
  private final PopupPanel popup;

  /**
   * Create a date box.
   */
  public DateBox() {
    this(DateTimeFormat.getFormat(PredefinedFormat.DATE_MEDIUM));
  }

  /**
   * Create a new date box.
   * 
   * @param format to use to parse and format dates
   * 
   */
  public DateBox(DateTimeFormat format) {
    this(format, new DatePicker());
  }

  /**
   * Create a new date box.
   * 
   * @param date the default date.
   * @param picker the picker to drop down from the date box
   * @param format to use to parse and format dates
   * 
   */
  public DateBox(DateTimeFormat format, DatePicker picker) {
    this(Document.get().createTextInputElement(), format, picker);
    setStyleName(DEFAULT_STYLENAME);
  }

  /**
   * This constructor may be used by subclasses to explicitly use an existing element. This element
   * must be a &lt;input&gt; element.
   * 
   * @param element the element to be used
   */
  public DateBox(Element element) {
    this(element, DateTimeFormat.getFormat(PredefinedFormat.DATE_MEDIUM), new DatePicker());
  }

  /**
   * Create a new date box.
   * 
   * @param date the default date.
   * @param picker the picker to drop down from the date box
   * @param format to use to parse and format dates
   * 
   */
  protected DateBox(Element element, DateTimeFormat format, DatePicker picker) {
    super(element, new DateTimeRenderer(format), new DateParser(format));
    this.picker = picker;
    dateTimeFormat = format;
    popup = new PopupPanel(true);
    popup.addAutoHidePartner(getElement());
    popup.setWidget(picker);
    popup.setStyleName("dateBoxPopup");
    DateBoxHandler handler = new DateBoxHandler();
    picker.addValueChangeHandler(handler);
    addFocusHandler(handler);
    addBlurHandler(handler);
    addClickHandler(handler);
    addKeyDownHandler(handler);
    popup.addCloseHandler(handler);
  }

  @Override
  public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Date> handler) {
    return addHandler(handler, ValueChangeEvent.getType());
  }

  /**
   * Gets the date picker.
   * 
   * @return the date picker
   */
  public DatePicker getDatePicker() {
    return picker;
  }

  /**
   * Gets the format instance used to control formatting and parsing of this {@link DateBox}.
   * 
   * @return the format
   */
  public DateTimeFormat getFormat() {
    return dateTimeFormat;
  }

  /**
   * Hide the date picker.
   */
  public void hideDatePicker() {
    popup.hide();
  }

  /**
   * Returns true if date picker is currently showing, false if not.
   */
  public boolean isDatePickerShowing() {
    return popup.isShowing();
  }

  /**
   * Parses the current date box's value and shows that date.
   */
  public void showDatePicker() {
    Date current = getValue();
    if (current == null) {
      current = new Date();
    }
    picker.setCurrentMonth(current);
    popup.showRelativeTo(this);
  }

  private void preventDatePickerPopup() {
    allowDPShow = false;
    Scheduler.get().scheduleDeferred(new Command() {
      @Override
      public void execute() {
        allowDPShow = true;
      }
    });
  }

  private void updateDateFromTextBox() {
    Date value = getValue();
    setValue(value);
    picker.setValue(value);
    ValueChangeEvent.fire(this, value);
  }
}
